import matplotlib.patches as mpatches
import numpy as np
import pandas as pd


class Track(object):

    """
    Track

    A Track is a collection of events. May have a `name` and a `color`.
    """

    def __init__(self, id, name, color):
        self.id = id
        self.name = name
        self.color = color
        self.events = []

    def __repr__(self):
        return "[ {} ] color: {} | events: {:>2} | name: {}".format(
            self.__class__.__name__, self.color, self.nevents, self.name)

    def add_event(self, event):
        """Add an event to the Track.
        If the event added has a property named `time`, the Track
        events are sorted accordingly.
        """
        self.events.append(event)
        self.events.sort(key=lambda event: event.time)

    def as_series(self, minimum=0, maximum=None):
        """Returns the annotated events as a ``pandas.Series``.

        Args:
            minimum (int): minimum time to consider
            maximum (int): maximum time to consider; defaults to last
                           event end time

        Returns:
            pandas.Series

        """
        maximum = maximum or self.events[-1].time_interval[1]
        series = pd.Series(0, index=range(minimum, maximum + 1), dtype=bool)
        for event in self.events:
            if event.duration > 0:
                series.loc[slice(*event.time_interval)] = True
            else:
                if event.time in series.index:
                    series.loc[event.time] = True
        return series

    def get_mask(self, max, min=0):
        """Get boolean mask.
        Returns a `numpy.array()` with True entries where there are
        events, and False otherwise.
        """
        mask = np.zeros(max - min + 1, dtype=bool)
        for event in self.events:
            if event.duration > 0:
                for t in range(*event.time_interval):
                    try:
                        mask[t] = True
                    except IndexError:
                        pass
            else:
                mask[event.time] = True
        return mask

    def intersection_with(self, track):
        """Intersect this track with another.
        Returns a list with all the times found in both tracks events
        lists.
        """

        def events_list_to_set(events):
            """Create a set with all event times and fill in those implicit
            by the events with a duration."""
            s = set()
            for event in events:
                if event.duration > 0:
                    for t in range(*event.time_interval):
                        s.add(t)
                else:
                    s.add(event.time)
            return s

        self_events = events_list_to_set(self.events)
        other_track_events = events_list_to_set(track.events)

        return sorted(list(self_events.intersection(other_track_events)))

    def get_duration(self, t, ti=0):
        """
        Get the annotated duration constrained to a time interval [ti, t].
        """
        sum_ = 0
        for e in self.events:
            begin, end = e.time_interval
            # Check first all the events that completly fit inside the interval
            if begin >= ti and end <= t:
                sum_ += e.duration
                continue
            # Events that cross the ti instant
            if begin < ti and end > ti:
                if end > t:
                    sum_ += t - ti
                else:
                    sum_ += end - ti
                continue
            # Events that cross the t instant
            if begin < t and end > t:
                sum_ += t - begin
                # This assumes that it went through the other conditions
                continue
        return sum_

    def get_full_duration(self):
        """Sums up all its events duration and returns that."""
        return sum([e.duration for e in self.events])

    def plot(self, ax, y=0, h=1, ignore_duration=True, offset=5, **kwargs):
        """
        Plot the Track in a given `Axes` object.

        Each event in the Track is represented by a Rectangle or by a
        vertical line, depending if you want to ignore its duration.

        Args:
            ax: maptlotlib Axes object to plot
            y: y position of the bottom edge /point
            h: height of the rectangle / line
            ignore_duration: if True, event duration is ignored
            offset: percentage to shrink the drawn patch

        Returns:
            Axes: maptlotlib Axes object

        .. warning:: Barely tested!

        """
        offset /= 100

        for event in self.events:

            if event.duration > 0 and ignore_duration is False:
                xy = (event.time, y + (h * offset))
                w = event.duration
                rect = mpatches.Rectangle(xy, w, h - 2 * (h * offset),
                                          lw=0, **kwargs)
                ax.add_patch(rect)
            else:
                if 'lw' not in kwargs.keys():
                    kwargs['lw'] = 1
                ax.vlines(event.time, y + (h * offset),
                          (y + h) - (h * offset), **kwargs)

        return ax

    @property
    def nevents(self):
        """Number of events."""
        return len(self.events)


class Event(object):

    """
    Event

    An event happening at time `t`. Can also last for a certain `duration`
    and have some `comment`.
    """

    def __init__(self, t, duration=0, comment=""):
        self.time = t
        self.duration = duration
        self.comment = comment

    def __repr__(self):
        t = self.time_interval if self.duration > 0 else self.time
        return "[ {} ] t={}".format(self.__class__.__name__, t)

    @property
    def time_interval(self):
        if self.duration > 0:
            return (self.time, self.time + self.duration)
        else:
            return 0
